/*****************************************************************************
 * $LastChangedDate: 2012-03-04 10:35:05 -0500 (Sun, 04 Mar 2012) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   ShaderFactory attaches a shader to a node.
 *//*
 * LEGAL:   COPYRIGHT (C) 2009 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

#define SHADER_SHADER_FACTORY_CC 1
#include <utility>
#include <sstream>
#include "base/module.hh"
#include "base/file.hh"
#include "base/conf.hh"
#include "base/stream.hh"
using namespace base;
#include "shader/module.hh"
#include "shader/conf.hh"
#include "shader/shader_factory.hh"

#include <osg/Program>
#include <osg/Shader>

namespace shader {

////////////////////////////////////////////////////////////////////////////////
///////////////////////////////  ShaderFactory  ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

ShaderFactory* ShaderFactory::msInstance;

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
ShaderFactory::ShaderFactory( void )
:   mShaderDir(GetDataDir() + string("/shaders/")),
    mShaderMap()
{
    // NOP
}

ShaderFactory::~ShaderFactory()
{
    // NOP
}

/*****************************************************************************
 * Attach a shader program to a node.
 * Uses a cache of pre-compiled shaders indexed by name.
 *****************************************************************************/
void
ShaderFactory::AttachShader( const ShaderName& shaderName, RefPtr<Node> node )
{
    // Are shaders disabled?
    if ( EX( SHADER_CONF.mEnableShaders ) )
    {
        // Attach shader to node.
        // Disable lighting by forcing the NOP shader.
        // But allow non-lighting shaders.
        const bool forceNopShader = (not SHADER_CONF.mEnableLighting)
                                and (shaderName.GetString() != "color");
        ShaderProgram shaderProgram = GetShader( not forceNopShader ? shaderName : ShaderName("nop") );
        node->getOrCreateStateSet()->setAttributeAndModes( shaderProgram.mProgram.get(), osg::StateAttribute::ON );
    }
}

/*****************************************************************************
 * Get shader program.
 *****************************************************************************/
ShaderProgram
ShaderFactory::GetShader( const ShaderName& shaderName )
{
    // Was shader program already compiled?
    ShaderMap::iterator iter = mShaderMap.find( shaderName );
    if ( iter != mShaderMap.end() )
    {
        // Was created before.
        return iter->second;
    }
    else
    {
        // Create shader.
        ShaderProgram shaderProgram = CreateShader( shaderName );

        // Cache shader.
        mShaderMap.insert( std::make_pair<>( shaderName, shaderProgram ) );

        return shaderProgram;
    }   
}

/*****************************************************************************
 * (private)
 * Unconditionally create/compile a shader.
 *****************************************************************************/
ShaderProgram
ShaderFactory::CreateShader( const ShaderName& shaderName )
{
    // Build pathnames to shader source code files.
    // Naming convention:
    // ${shaderName}_vert.glsl
    // ${shaderName}_frag.glsl
    const string vertPathName = mShaderDir + shaderName.GetString() + string("_vert.glsl");
    const string fragPathName = mShaderDir + shaderName.GetString() + string("_frag.glsl");

    // Read shader src into C++ strings.
    string vertShaderSrcRaw;
    string fragShaderSrcRaw;
    if ( (not ReadFile( vertPathName, vertShaderSrcRaw ))
      or (not ReadFile( fragPathName, fragShaderSrcRaw )) )
    {
        throw Exception( string("cannot read shader src: ") + shaderName.GetString() );
    }

    // Expands custom macros in GLSL source.
    const string vertShaderSrc = ExpandMacros( vertShaderSrcRaw );
    const string fragShaderSrc = ExpandMacros( fragShaderSrcRaw );

    // Create shader program.
    // OSG will automaticallly compile/link shader program.
    ShaderProgram shaderProgram( new osg::Program,
                                 new osg::Shader( osg::Shader::VERTEX,   vertShaderSrc ),
                                 new osg::Shader( osg::Shader::FRAGMENT, fragShaderSrc ) );

    shaderProgram.mProgram->setName( shaderName.GetString() );
    shaderProgram.mProgram->addShader( shaderProgram.mVertexShader.get() );
    shaderProgram.mProgram->addShader( shaderProgram.mFragmentShader.get() );

    return shaderProgram;
}

/*****************************************************************************
 * Recursively expand macros in shader source.
 * Tests for this is at: tests/shader/macro.cc
 *****************************************************************************/
string
ShaderFactory::ExpandMacros( const string& shaderSrc )
{
    std::istringstream inputStream( shaderSrc );
    std::ostringstream outputStream;
    string field;

    // Scan for macro commands.
    while ( inputStream.good() )
    {
        field.erase(); inputStream >> field;
        if ( field == "@include" )
        {
            field.erase(); inputStream >> field;
            const string includeFilename = mShaderDir + field;
            string includeRaw;
            if ( UX( not ReadFile( includeFilename, includeRaw ) ) )
            {
                std::ostringstream msg;
                msg << "ERROR[shader:@include]: cannot read " << includeFilename << std::endl;
                throw Exception( msg.str() );
            }

            // Recurse!
            string include = ExpandMacros( includeRaw );
            outputStream << include;  // expand include file
        }
        else  // not a macro keyword
        {
            outputStream << field;
        }

        // Preserve whitespace in source code.
        while ( inputStream.good() )
        {
            const char c = inputStream.peek();
            if ( EX( c == ' ' or c == '\t' or c == '\n' ) )
            {
                outputStream << c;  // copy to output
                inputStream.get();  // discard next whitespace char
            }
            else
            {
                break;  // non-whitespace
            }
        }
    }

    // Yuck but cannot force ostringstream() ctor to use specified string
    // [ostringstream(const string&) implies a copy).
    return outputStream.str();
}

/*****************************************************************************
 * Flush cache of shader programs.
 *****************************************************************************/
void
ShaderFactory::Flush( void )
{
    mShaderMap.clear();
}

} // namespace shader
